/*
 * Copyright (c) 2000, 2012, Oracle and/or its affiliates. All rights reserved.
 * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 *
 */

package javax.imageio.stream;

import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.io.RandomAccessFile;
import java.nio.file.Files;
import com.sun.imageio.stream.StreamCloser;

/**
 * An implementation of <code>ImageOutputStream</code> that writes its
 * output to a regular <code>OutputStream</code>.  A file is used to
 * cache data until it is flushed to the output stream.
 */
public class FileCacheImageOutputStream extends ImageOutputStreamImpl {

  private OutputStream stream;

  private File cacheFile;

  private RandomAccessFile cache;

  // Pos after last (rightmost) byte written
  private long maxStreamPos = 0L;

  /**
   * The CloseAction that closes the stream in
   * the StreamCloser's shutdown hook
   */
  private final StreamCloser.CloseAction closeAction;

  /**
   * Constructs a <code>FileCacheImageOutputStream</code> that will write
   * to a given <code>outputStream</code>.
   *
   * <p> A temporary file is used as a cache.  If
   * <code>cacheDir</code>is non-<code>null</code> and is a
   * directory, the file will be created there.  If it is
   * <code>null</code>, the system-dependent default temporary-file
   * directory will be used (see the documentation for
   * <code>File.createTempFile</code> for details).
   *
   * @param stream an <code>OutputStream</code> to write to.
   * @param cacheDir a <code>File</code> indicating where the cache file should be created, or
   * <code>null</code> to use the system directory.
   * @throws IllegalArgumentException if <code>stream</code> is <code>null</code>.
   * @throws IllegalArgumentException if <code>cacheDir</code> is non-<code>null</code> but is not a
   * directory.
   * @throws IOException if a cache file cannot be created.
   */
  public FileCacheImageOutputStream(OutputStream stream, File cacheDir)
      throws IOException {
    if (stream == null) {
      throw new IllegalArgumentException("stream == null!");
    }
    if ((cacheDir != null) && !(cacheDir.isDirectory())) {
      throw new IllegalArgumentException("Not a directory!");
    }
    this.stream = stream;
    if (cacheDir == null) {
      this.cacheFile = Files.createTempFile("imageio", ".tmp").toFile();
    } else {
      this.cacheFile = Files.createTempFile(cacheDir.toPath(), "imageio", ".tmp")
          .toFile();
    }
    this.cache = new RandomAccessFile(cacheFile, "rw");

    this.closeAction = StreamCloser.createCloseAction(this);
    StreamCloser.addToQueue(closeAction);
  }

  public int read() throws IOException {
    checkClosed();
    bitOffset = 0;
    int val = cache.read();
    if (val != -1) {
      ++streamPos;
    }
    return val;
  }

  public int read(byte[] b, int off, int len) throws IOException {
    checkClosed();

    if (b == null) {
      throw new NullPointerException("b == null!");
    }
    if (off < 0 || len < 0 || off + len > b.length || off + len < 0) {
      throw new IndexOutOfBoundsException
          ("off < 0 || len < 0 || off+len > b.length || off+len < 0!");
    }

    bitOffset = 0;

    if (len == 0) {
      return 0;
    }

    int nbytes = cache.read(b, off, len);
    if (nbytes != -1) {
      streamPos += nbytes;
    }
    return nbytes;
  }

  public void write(int b) throws IOException {
    flushBits(); // this will call checkClosed() for us
    cache.write(b);
    ++streamPos;
    maxStreamPos = Math.max(maxStreamPos, streamPos);
  }

  public void write(byte[] b, int off, int len) throws IOException {
    flushBits(); // this will call checkClosed() for us
    cache.write(b, off, len);
    streamPos += len;
    maxStreamPos = Math.max(maxStreamPos, streamPos);
  }

  public long length() {
    try {
      checkClosed();
      return cache.length();
    } catch (IOException e) {
      return -1L;
    }
  }

  /**
   * Sets the current stream position and resets the bit offset to
   * 0.  It is legal to seek past the end of the file; an
   * <code>EOFException</code> will be thrown only if a read is
   * performed.  The file length will not be increased until a write
   * is performed.
   *
   * @throws IndexOutOfBoundsException if <code>pos</code> is smaller than the flushed position.
   * @throws IOException if any other I/O error occurs.
   */
  public void seek(long pos) throws IOException {
    checkClosed();

    if (pos < flushedPos) {
      throw new IndexOutOfBoundsException();
    }

    cache.seek(pos);
    this.streamPos = cache.getFilePointer();
    maxStreamPos = Math.max(maxStreamPos, streamPos);
    this.bitOffset = 0;
  }

  /**
   * Returns <code>true</code> since this
   * <code>ImageOutputStream</code> caches data in order to allow
   * seeking backwards.
   *
   * @return <code>true</code>.
   * @see #isCachedMemory
   * @see #isCachedFile
   */
  public boolean isCached() {
    return true;
  }

  /**
   * Returns <code>true</code> since this
   * <code>ImageOutputStream</code> maintains a file cache.
   *
   * @return <code>true</code>.
   * @see #isCached
   * @see #isCachedMemory
   */
  public boolean isCachedFile() {
    return true;
  }

  /**
   * Returns <code>false</code> since this
   * <code>ImageOutputStream</code> does not maintain a main memory
   * cache.
   *
   * @return <code>false</code>.
   * @see #isCached
   * @see #isCachedFile
   */
  public boolean isCachedMemory() {
    return false;
  }

  /**
   * Closes this <code>FileCacheImageOutputStream</code>.  All
   * pending data is flushed to the output, and the cache file
   * is closed and removed.  The destination <code>OutputStream</code>
   * is not closed.
   *
   * @throws IOException if an error occurs.
   */
  public void close() throws IOException {
    maxStreamPos = cache.length();

    seek(maxStreamPos);
    flushBefore(maxStreamPos);
    super.close();
    cache.close();
    cache = null;
    cacheFile.delete();
    cacheFile = null;
    stream.flush();
    stream = null;
    StreamCloser.removeFromQueue(closeAction);
  }

  public void flushBefore(long pos) throws IOException {
    long oFlushedPos = flushedPos;
    super.flushBefore(pos); // this will call checkClosed() for us

    long flushBytes = flushedPos - oFlushedPos;
    if (flushBytes > 0) {
      int bufLen = 512;
      byte[] buf = new byte[bufLen];
      cache.seek(oFlushedPos);
      while (flushBytes > 0) {
        int len = (int) Math.min(flushBytes, bufLen);
        cache.readFully(buf, 0, len);
        stream.write(buf, 0, len);
        flushBytes -= len;
      }
      stream.flush();
    }
  }
}
